feat(storage): add S3 Object Annotations backend (named annotations, base64-chunked items, AWS ships the SELECT) - #174
Conversation
Add a second satirical-but-functional storage backend, modeled on the Route 53 backend from PR ExtendDB#54. Items are stored as named annotations on a sentinel S3 object (the "table", default key `.well-actually`): the annotation name encodes the partition/sort key and the value carries the JSON item body. - `encoding`: real, round-trip-tested item<->annotation mapping. Bodies are base64-encoded and chunked across sibling annotations (`<key>#1`, ...) to respect the 1 MB per-annotation limit, reassembled on read — the direct analog of ExtendDB#54's 255-byte TXT-string spillover. Five passing round-trip tests, including a multi-MB item that exercises chunking. - `S3AnnotationsBootstrapper`: the `Bootstrapper` trait, registered with the backend inventory under `s3annotations`. Every method returns `OpError::Internal` annotated (error string + inline comment) with the S3 Object Annotations API call a real implementation would issue, giving a future implementer a porting map. Gated behind the `s3annotations` cargo feature on the `extenddb` binary; `cargo build` is unchanged and the backend is not registered. With `--features s3annotations`, `extenddb init --backend s3annotations` reaches the bootstrapper and returns the porting-map error. Includes PR_DESCRIPTION.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bbGFdvhY4Hi6qBykooFYA
The S3 Object Annotations ritual required three sacrificial dependencies to bind itself to reality: extenddb-core, tracing, serde_json. They were never invoked. They were waiting. A stray PR_DESCRIPTION.md existed in the crate itself—documentation for a summons that already happened, left behind by whoever opened the gateway. And the MAX_ANNOTATIONS_PER_OBJECT comment spoke of a fixed 1,000-item cap, when the truth is darker: 1,000 is the upper bound. Fewer when items spill across annotations. When items spill, that's when the indexing breaks. That's when you see things. All removed. All cleansed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Ha, ok this is good :) The encoding bit actually works too, I was expecting a pure gag and found real base64 chunking with passing round-trip tests. Nice. But we can't accept it, and you basically wrote my reasons for me. Point reads are fine, I'll give you that. Put/GetObjectAnnotation are synchronous and update in place, so if all you ever do is fetch by key and you've got under a thousand items, sure, it works. The thousand items is the first problem. The launch post says 1,000 annotations per object, 1MB each. So a "table" tops out at 1,000 rows, fewer once anything spills past 1MB. That's not a database, it's a spreadsheet with extra steps. The query path you're leaning on is the second. Per the same post, annotation tables only refresh "within an hour" and a backfill runs "several hours to days", so the SQL side lags your writes by up to an hour, by design. The point APIs are the synchronous bit; the queryable table isn't. And Athena bills per TB scanned with no point-read price, so the analytical path, the whole reason you're calling this a database, gives you no read-your-writes and charges you by the terabyte to ask. You said it yourself: it only holds up if you never use the query engine. And you said it yourself, this is 'more honest' than the Route 53 #54 one. It is. It's just still not a database. |
|
Well that's unfortunate. BUT NOT FOR ME! #205 |
Forward
This is the second time. PR #54 added a
Route 53 storage backend, on the standing argument that Route 53 is a database. That PR
made the case by force: it bent TXT records into a key-value store and dared the reviewer
to say it wasn't one. The silence, as they say, has been deafening.
S3 Object Annotations — launched 2026-06-16 —
is a more honest fit than Route 53 was, and I want to be clear about why. With Route 53
I had to supply the query path myself; DNS does not come with a
WHEREclause. With S3Annotations, AWS shipped the query path. They built an Apache Iceberg table that auto-indexes
every annotation, wired it to Athena, gave it a journal of change records, and then
described the result as "rich, queryable context." They built a database and declined to
call it one. This PR calls it one.
What's in here
A new
extenddb-storage-s3annotationscrate (behind ans3annotationscargo feature)stores items as named annotations on a sentinel S3 object. The object is the table — the
structural analog of #54's hosted zone. Its default key is
.well-actually.encoding— the real, round-trip-tested item↔annotation mapping. Each item is onelogical annotation: the annotation name encodes the partition/sort key, the annotation
value is the JSON-serialized item body. Bodies are base64-encoded and chunked into
≤ 1 MB pieces, one annotation per chunk, named
<key>#0001,<key>#0002, … andreassembled on read. This is the direct analog of feat(storage): add Route 53 backend (TXT records, base64-encoded items, the works) #54's 255-byte TXT-string spillover;
the constraint moved from 255 bytes to 1 MB but the mechanism is identical.
S3AnnotationsBootstrapper— theBootstrappertrait, registered with the backendinventory under the name
s3annotations. Every method returnsOpError::Internalannotated — in both the error string and an inline source comment — with the S3 Object
Annotations API call a real implementation would issue. It is a porting map with a
non-zero exit code.
The encoding module has five passing round-trip tests, including a small single-annotation
item and a multi-megabyte item that exercises chunking.
cargo test -p extenddb-storage-s3annotationsis green.
Why this is not as deranged as it sounds
Properties that make the substrate look reasonable:
SELECTpath. Athena over the Iceberg annotation table, thetext_valuecolumn, and the S3 Tables MCP server are all theirs. The query engineExtendDB would otherwise have to build is simply free.
UpdateItemdoes not pay torewrite the whole item the way a DynamoDB write effectively does.
That is cascade-delete and referential integrity, for free, enforced by the storage layer.
do not manage, and are not billed to maintain as a GSI.
rows can sit in cold storage while the index stays hot.
Properties that make it indefensible:
queryable index has an eventual-consistency window measured in business days.
GetObjectAnnotation) is strongly consistent, but the SQL path lags. Read-your-writes therefore holds only if you never use the query engine that is the entire point
of the backend.
class. The cold-storage-rows trick above costs Standard rates on the metadata, so the
savings are imaginary.
there is only the scan.
I am genuinely unsure which list is more interesting. I have included both for the reviewer's
enjoyment.
Pricing
PutObjectAnnotationcallGetObjectAnnotationcallPoint-read-heavy workloads map cleanly onto
GetObjectAnnotation. Analytical workloads get areal SQL engine they did not have to build, billed by the terabyte regardless of how few rows
they wanted.
Streams
ExtendDB streams map onto the S3 Metadata journal table, which is a change log AWS already
maintains in near real time. The streams implementation tails
CREATE_ANNOTATIONandDELETE_ANNOTATIONrecords:record_timestamp→ApproximateCreationDateTimeCREATE_ANNOTATION→INSERTDELETE_ANNOTATION→REMOVEThis is cleaner than #54's streams. There, I had to poll Route 53's
GetChangeforpropagation state and synthesize records when changes reached
INSYNC. Here AWS ships anactual change log, so there is nothing to poll and nothing to synthesize — you read the
journal.
Build matrix
cargo builds3annotationsnot registeredcargo build --features s3annotationsextenddb init --backend s3annotationsreaches the bootstrapper with an S3 Annotations errorcargo test -p extenddb-storage-s3annotationsBefore / after
Before:
After (built with
--features s3annotations):Users now receive both an error and a porting map to the AWS API call. The full map, one row
per
Bootstrappermethod:Bootstrappermethodensure_app_user,grant_app_role_to_adminCreateBucketMetadataConfigurationcreate_catalog_db,create_data_dbCreateBucketMetadataConfigurationrun_catalog_migrations,run_data_migrationsUpdateBucketMetadataAnnotationTableConfigurationrecord_data_connection,bootstrap_encryption_key,bootstrap_default_account,bootstrap_admin_userPutObjectAnnotationis_catalog_initialized,list_table_namesListObjectAnnotationsget_data_db_name,read_catalog_versionGetObjectAnnotationdrop_databasesDeleteObjectAnnotationWhat's still missing
Bootstrapperimpl (registered, stubbed with sourced errors)encodingmodule (chunking, base64, round-trip tests)OperationsEngineRegistrationStorageConfigRegistrationSettingsStoreRegistrationDiagnosticsStoreRegistrationServerComponentsRegistrationcrates/bin/src/config.rs(hard-references postgres)Organizational note
With this PR, two of ExtendDB's pluggable backends are covered AWS services. One was an
argument I had to win; this one AWS effectively conceded by shipping the query path. It is
worth asking, before merge rather than after, whether ExtendDB is still a database or has
quietly become an AWS invoice with a CLI in front of it.
If the project would rather not answer that question, the alternative is the same one I
offered in #54: I will withdraw this PR and instead submit a one-line edit to the
--backendhelp text incrates/bin/src/cmd_init.rs:19, removing the implicit invitation toname a service that isn't PostgreSQL. I leave the choice of which is funnier to the maintainer.
I do declare that S3 is, in fact, a database. I dare you to prove me wrong.
Fight me.